package xyz.scootaloo.mono.security.filter

import cn.hutool.log.LogFactory
import org.apache.shiro.authc.AuthenticationException
import org.apache.shiro.authc.AuthenticationToken
import org.apache.shiro.subject.Subject
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
import org.apache.shiro.web.util.WebUtils
import org.springframework.web.bind.annotation.RequestMethod
import xyz.scootaloo.mono.base.lang.R
import xyz.scootaloo.mono.base.lang.SC
import xyz.scootaloo.mono.security.SecurityAutoConfiguration
import xyz.scootaloo.mono.security.realm.JwtRealm
import xyz.scootaloo.mono.security.service.AccountService
import xyz.scootaloo.mono.security.token.ClientID
import xyz.scootaloo.mono.security.token.JwtToken
import xyz.scootaloo.mono.security.util.JwtHelper
import xyz.scootaloo.mono.security.util.KW_HTTP
import xyz.scootaloo.mono.security.util.KW_PATH
import xyz.scootaloo.mono.security.util.KW_STATUS
import javax.servlet.ServletRequest
import javax.servlet.ServletResponse
import javax.servlet.http.HttpServletRequest

/**
 * 无状态 web 路径保护的主要实现
 *
 * shiro中, 标记为 "authc" 的路径, 都会经过此过滤器,
 * see [SecurityAutoConfiguration.createFilterChainDefinitionMap]
 *
 * 拦截请求, 检查请求中携带的jwt, 经过校验后, 提取其中有关角色的信息, 然后将角色信息绑定到对应的subject上,
 * 随后, 会有其他的过滤来检查是否有访问路径需要的角色, 决定是否放行
 *
 * @author flutterdash@qq.com
 * @since 2021/7/25 21:30
 */
class BonJwtFilter(
    private var accountService: AccountService
) : BasicHttpAuthenticationFilter() {

    private val log = LogFactory.get()

    init {
        loginUrl = KW_PATH.login
    }

    /**
     * 预处理
     */
    override fun preHandle(request: ServletRequest, response: ServletResponse): Boolean {
        val httpServletRequest = WebUtils.toHttp(request)
        if (httpServletRequest.method.equals(RequestMethod.OPTIONS.name))
            return false
        return super.preHandle(request, response)
    }

    /**
     * 核心逻辑, 决定是否放行
     */
    override fun isAccessAllowed(
        request: ServletRequest, response: ServletResponse, mappedValue: Any?
    ): Boolean {
        if (isLoginRequest(request, response))
            return true
        return isPermissive(mappedValue) || try {
            /** see [createToken] */
            /** see [JwtRealm.doGetAuthenticationInfo]  */
            executeLogin(request, response)
        } catch (e: IllegalStateException) {
            log.error("未找到任何token")
            false
        } catch (e: Exception) {
            log.error("登陆时遇到异常", e)
            return false
        }
    }

    override fun createToken(
        request: ServletRequest, response: ServletResponse
    ): AuthenticationToken? {
        val token = getAuthzFromHeader(request)
        return if (token != null && token.isNotBlank()) {
            JwtToken(token, WebUtils.toHttp(request), WebUtils.toHttp(response))
        } else {
            accountService.writeJsonBody(response, R.fail(SC.NOT_FOUND_TOKEN))
            null
        }
    }

    /**
     * 登陆成功后, 将请求报文中的token信息写入到响应报文中
     */
    override fun onLoginSuccess(
        token: AuthenticationToken?,
        subject: Subject,
        request: ServletRequest,
        response: ServletResponse
    ): Boolean {
        val httpResponse = WebUtils.toHttp(response)
        if (token is JwtToken) {
            if (shouldRefreshToken(token.token)) {
                val client = subject.principal as ClientID
                val newJwt = accountService.signNewJWT(request as HttpServletRequest, client)
                httpResponse.setHeader(KW_HTTP.JWT_MARK, newJwt)
            } else {
                httpResponse.setHeader(KW_HTTP.JWT_MARK, token.token)
            }
        }
        return true
    }

    override fun onLoginFailure(
        token: AuthenticationToken?,
        e: AuthenticationException?,
        request: ServletRequest,
        response: ServletResponse
    ): Boolean {
        log.error("验证token失败, token:$token, error:${e?.message}")
        return false
    }

    override fun onAccessDenied(request: ServletRequest, response: ServletResponse): Boolean {
        return WebUtils.toHttp(response).run {
            /**
             * bug: 向response写入body, 而此处的编码设置未生效
             * 编码设置失效的问题的解决方案
             * see [AccountService.writeJsonBody]
             */
            characterEncoding = KW_HTTP.CHARSET_ENCODING
            contentType = KW_HTTP.JSON_CONTENT_TYPE
            status = KW_STATUS.SC_NON_AUTHORITATIVE_INFORMATION
            false
        }
    }

    /**
     * 提取 http 请求头中的 token 信息
     */
    private fun getAuthzFromHeader(request: ServletRequest): String? {
        val httpRequest = WebUtils.toHttp(request)
        return httpRequest.getHeader(KW_HTTP.JWT_MARK)
    }

    private fun shouldRefreshToken(token: String): Boolean =
        JwtHelper.isTokenExpired(token)
}
